package com.cyw.common.utils.http;

import org.apache.http.HttpHost;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

/**
 * 自定义httpClient
 */
public class HttpClientBuilder {
    private static final int POOL_MAX_TOTAL = 100;
    private static final int CONNECT_TIMEOUT = 8000;
    private static final int SOCKET_TIMEOUT = 30000;
    private static final int CONNECTION_REQUEST_TIMEOUT = 8000;

    private int poolMaxTotal = POOL_MAX_TOTAL;
    private int connectTimeout = CONNECT_TIMEOUT;
    private int socketTimeout = SOCKET_TIMEOUT;
    /**
     * 是否使用安全的httpclient
     */
    private boolean secure = true;
    /**
     * 代理主机
     */
    private HttpHost proxy = null;
    /**
     * proxy是否是socks代理
     */
    private boolean socksProxy = false;
    /**
     * 是否启用重定向，启用之后httpclient会自动发送重定向请求
     */
    private boolean redirectEnable = true;

    /**
     * 创建builder
     * @return
     */
    public static HttpClientBuilder create() {
        return new HttpClientBuilder();
    }

    public int getPoolMaxTotal() {
        return poolMaxTotal;
    }

    public HttpClientBuilder withPoolMaxTotal(int poolMaxTotal) {
        this.poolMaxTotal = poolMaxTotal;
        return this;
    }

    public int getConnectTimeout() {
        return connectTimeout;
    }

    public HttpClientBuilder withConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
        return this;
    }

    public int getSocketTimeout() {
        return socketTimeout;
    }

    public HttpClientBuilder withSocketTimeout(int socketTimeout) {
        this.socketTimeout = socketTimeout;
        return this;
    }

    public boolean isSecure() {
        return secure;
    }

    public HttpClientBuilder withSecure(boolean secure) {
        this.secure = secure;
        return this;
    }

    public HttpHost getProxy() {
        return proxy;
    }

    public HttpClientBuilder withProxy(HttpHost proxy) {
        this.proxy = proxy;
        return this;
    }

    public boolean isSocksProxy() {
        return socksProxy;
    }

    public HttpClientBuilder withSocksProxy(boolean socksProxy) {
        this.socksProxy = socksProxy;
        return this;
    }

    public boolean isRedirectEnable() {
        return redirectEnable;
    }

    public HttpClientBuilder withRedirectEnable(boolean redirectEnable) {
        this.redirectEnable = redirectEnable;
        return this;
    }

    /**
     * 获取定制的httpclient
     * 注意： 每创建一个HttpClient都会创建一个后台线程清理过期连接或超时连接
     * @return
     */
    public CloseableHttpClient build() {
        //自定义的socket工厂类可以和指定的协议（Http、Https）联系起来，用来创建自定义的连接管理器。
        RegistryBuilder<ConnectionSocketFactory> r = RegistryBuilder.create();

        SSLContext sslContext = null;
        try {
            if (secure) {
                sslContext = new SSLContextBuilder().build();
            } else {
                //https信任所有证书
                sslContext = new SSLContextBuilder().loadTrustMaterial(null, (chain, authType) -> true).build();
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        }

        HostnameVerifier hostnameVerifier = (s, sslSession) -> true;
        boolean useSocksProxy = proxy != null && socksProxy;

        SSLConnectionSocketFactory sslsf = useSocksProxy
                ? new SocksSSLConnectionSocketFactory(proxy, sslContext, hostnameVerifier)
                : new SSLConnectionSocketFactory(sslContext, hostnameVerifier);

        PlainConnectionSocketFactory plainsf = useSocksProxy
                ? new SocksConnectionSocketFactory(proxy)
                : PlainConnectionSocketFactory.getSocketFactory();

        r = r.register("https", sslsf);
        r = r.register("http", plainsf);

        //连接池管理器
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(r.build());
        connectionManager.setMaxTotal(poolMaxTotal);

        // 代理的socket
        if (proxy != null) {
            connectionManager.setSocketConfig(proxy, SocketConfig.custom().setSoTimeout(socketTimeout).build());
        }

        //cookie
        CookieStore cookieStore = new BasicCookieStore();

        org.apache.http.impl.client.HttpClientBuilder httpClientBuilder = HttpClients.custom()
                // 需要注意这两个配置会导致后台线程的创建
                .evictExpiredConnections()
                //.evictIdleConnections(30L, TimeUnit.SECONDS) // 影响清理线程的sleepTime和idleTime

                .setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE)//默认重试策略也是这样
                .setConnectionManager(connectionManager)
                .setDefaultCookieStore(cookieStore);

        RequestConfig.Builder builder = RequestConfig.custom();
        if (proxy != null && !socksProxy) {
            builder.setProxy(proxy);
        }
        RequestConfig globalconfig = builder
                .setRedirectsEnabled(redirectEnable)
                .setCookieSpec(CookieSpecs.DEFAULT)
                // 从连接池中获取连接的超时时间
                .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
                // 与服务器连接超时时间
                .setConnectTimeout(connectTimeout)
                // socket读数据超时时间：从服务器获取响应数据的超时时间
                .setSocketTimeout(socketTimeout)
                .build();

        return httpClientBuilder.setDefaultRequestConfig(globalconfig).build();
    }

    private static class SocksConnectionSocketFactory extends PlainConnectionSocketFactory {
        private HttpHost proxy;

        public SocksConnectionSocketFactory(HttpHost proxy) {
            this.proxy = proxy;
        }

        @Override
        public Socket createSocket(HttpContext context) throws IOException {
            return new Socket(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxy.getHostName(), proxy.getPort())));
        }

        @Override
        public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress,
                                    InetSocketAddress localAddress, HttpContext context) throws IOException {
            //make proxy server to resolve host in http url
            remoteAddress = InetSocketAddress.createUnresolved(host.getHostName(), host.getPort());
            return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
        }
    }

    private static class SocksSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
        private HttpHost proxy;

        public SocksSSLConnectionSocketFactory(HttpHost proxy, SSLContext sslContext, HostnameVerifier hostnameVerifier) {
            super(sslContext, hostnameVerifier);
            this.proxy = proxy;
        }

        @Override
        public Socket createSocket(HttpContext context) throws IOException {
            return new Socket(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxy.getHostName(), proxy.getPort())));
        }

        @Override
        public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress,
                                    InetSocketAddress localAddress, HttpContext context) throws IOException {
            //make proxy server to resolve host in http url
            remoteAddress = InetSocketAddress.createUnresolved(host.getHostName(), host.getPort());
            return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
        }
    }
}
